package org.hepeng.workx.mybatis.event.consumer;

import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.hepeng.workx.constant.ExecuteMode;
import org.hepeng.workx.extension.XLoader;
import org.hepeng.workx.util.ClassLoaderUtils;
import org.hepeng.workx.util.ClassUtils;
import org.hepeng.workx.util.proxy.Invocation;
import org.hepeng.workx.util.proxy.ProxyFactory;
import org.hepeng.workx.util.proxy.Invoker;
import org.reflections.Reflections;
import org.reflections.util.ConfigurationBuilder;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author he peng
 */
public class EventConsumerRegistry {

    protected static ProxyFactory proxyFactory = XLoader.getXLoader(ProxyFactory.class).getX();

    private final SetMultimap<Class<? extends Object> , EventConsumer> EVENT_CONSUMER_MAP = Multimaps.synchronizedSortedSetMultimap(Multimaps.newSortedSetMultimap(new ConcurrentHashMap<>(), () -> new TreeSet<>()));

    public void addConsumers(String ... packages) {
        addConsumers(true , packages);
    }

    public void addConsumers(boolean isOnlySpecifiedPackage , String ... packages) {
        if (ArrayUtils.isEmpty(packages)) {
            return;
        }
        if (isOnlySpecifiedPackage) {
            addConsumersOnlySpecifiedPackage(packages);
        } else {
            addConsumersIncludeSubPackage(packages);
        }
    }

    private void addConsumersOnlySpecifiedPackage(String ... packages) {
        if (ArrayUtils.isEmpty(packages)) {
            return;
        }

        for (String pkg : packages) {
            if (StringUtils.isBlank(pkg)) continue;
            ConfigurationBuilder config = new ConfigurationBuilder();
            config.filterInputsBy(input -> {
                if (StringUtils.endsWith(input , ".class")) {
                    int suffixIndex = StringUtils.indexOf(input, ".class");
                    String fullClassName = StringUtils.replaceAll(StringUtils.substring(input, 0, suffixIndex) , "/" , ".");
                    String packageName = StringUtils.substring(fullClassName , 0 , StringUtils.lastIndexOf(fullClassName, "."));
                    if (StringUtils.equals(packageName , pkg)) {
                        return true;
                    }
                    return false;
                }
                return false;
            });

            config.forPackages(pkg);
            Reflections reflections = new Reflections(config);
            addConsumers(reflections.getSubTypesOf(EventConsumer.class));
        }
    }

    private void addConsumersIncludeSubPackage(String ... packages) {
        ConfigurationBuilder config = new ConfigurationBuilder();
        config.filterInputsBy(input -> {
            if (StringUtils.endsWith(input , ".class")) {
                try {
                    int suffixIndex = StringUtils.indexOf(input, ".class");
                    Class<?> aClass = Class.forName(StringUtils.substring(input , 0 , suffixIndex) , true , ClassLoaderUtils.getSystemClassLoader());
                    if (Modifier.isAbstract(aClass.getModifiers())) {
                        return false;
                    }
                    return true;
                } catch (ClassNotFoundException e) {

                }
            }
            return false;
        });

        config.forPackages(packages);
        Reflections reflections = new Reflections(config);
        addConsumers(reflections.getSubTypesOf(EventConsumer.class));
    }


    public void addConsumers(Set<Class<? extends EventConsumer>> consumerClasses) {
        if (CollectionUtils.isEmpty(consumerClasses)) {
            return;
        }

        consumerClasses.forEach(consumerClass -> {
            if (! ClassUtils.isAbstract(consumerClass)) {
                addConsumer(consumerClass);
            }
        });
    }

    public void addConsumer(Class<? extends EventConsumer> consumerClass) {
        MyBatisEventConsumer myBatisEventConsumer = consumerClass.getAnnotation(MyBatisEventConsumer.class);
        EventConsumer consumer;
        if (Objects.nonNull(myBatisEventConsumer)) {
            consumer = newConsumer(myBatisEventConsumer.executeMode() , myBatisEventConsumer.order() , consumerClass);
        } else {
            consumer = newConsumer(ExecuteMode.ASYNC , Integer.MAX_VALUE ,consumerClass);
        }

        Class<?> genericClass;


        if (ArrayUtils.contains(consumerClass.getInterfaces() , EventConsumer.class)) {
            genericClass = ClassUtils.getSingleGeneric(consumerClass , EventConsumer.class);
        } else {
            genericClass = Object.class;
        }

        if (Objects.isNull(genericClass)) {
            genericClass = Object.class;
        }

        EVENT_CONSUMER_MAP.put(genericClass, consumer);

    }

    public Set<EventConsumer> getConsumer(Class<?> key) {
        if (Objects.isNull(key)) {
            return new TreeSet<>();
        }
        return EVENT_CONSUMER_MAP.get(key);
    }

    public Set<Map.Entry<Class<?>, EventConsumer>> entriesConsumer() {
        return EVENT_CONSUMER_MAP.entries();
    }

    private EventConsumer newConsumer(ExecuteMode executeMode, int order, Class<? extends EventConsumer> consumerClass) {
        List<Class<?>> interfaces = new ArrayList<>();
        interfaces.add(Comparable.class);

        Invoker invoker = new Invoker() {
            @Override
            public Object invoke(Invocation invocation) throws Throwable {
                Method method = invocation.getMethod();
                if (StringUtils.equals(method.getName(), "order")) {
                    return order;
                }

                if (StringUtils.equals(method.getName(), "getExecuteMode")) {
                    return executeMode;
                }

                if (StringUtils.equals(method.getName() , "compareTo")) {
                    return Objects.compare((EventConsumer)invocation.getProxy() ,
                            (EventConsumer)invocation.getArgs()[0] , (c1, c2) -> {
                                int result = Integer.valueOf(c1.order()).compareTo(c2.order()) * -1;
                                return result == 0 ? 1 : result;
                            });
                }

                return invocation.invoke();
            }
        };
        List<Invoker> invokers = new LinkedList<>();
        invokers.add(invoker);

        EventConsumer consumer = (EventConsumer) proxyFactory.createProxy(consumerClass, interfaces , null, null , invokers , null);
        return consumer;
    }

}
